package com.cordys.opensource.crom;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;

import com.cordys.cpc.bsf.busobject.BSF;
import com.cordys.cpc.bsf.busobject.BusObject;
import com.cordys.cpc.bsf.busobject.exception.BsfRuntimeException;
import com.eibus.soap.ApplicationConnector;
import com.eibus.soap.ApplicationTransaction;
import com.eibus.soap.BodyBlock;
import com.eibus.soap.Processor;
import com.eibus.soap.SOAPTransaction;
import com.eibus.xml.nom.Node;
import com.eibus.xml.xpath.XPath;
import com.eibus.xml.xpath.XPathMetaInfo;

/**
 * This connector is not a true connector in the sense that it can handle soap requests.
 * It is more of a plugin on top of WS-AppServer. This connector should always be configured
 * in the context of WS-AppServer.
 */
public class CROMConnector extends ApplicationConnector implements ApplicationTransaction
{
	/**
	 * Contains the list of CROMConnectors active in the current JVM.
	 * This is required to be able to do lookups in a static manner.
	 */
	private static final Map<String, CROMConnector> activeCroms = new HashMap<String, CROMConnector>();
	
	/**
	 * The soap processor in which current crom is hosted.
	 */
	private Processor soapProcessor;
	/**
	 * A string containing all errors that are found during initialization of crom. Crom tries to find as many
	 * as possible errors at once. E.g., if multiple relationships in database are not filled, crom will try to
	 * list all of those issues at once, so that you can read the log once, and do not have to restart and restart continuously to
	 * find out about all errors.
	 */
    private String configurationErrors;
    /**
     * Flag indicating whether crom initialization should be disabled. When we have made erros in code or in the database
     * schema, we may have to reload database metadata. But crom may be too eager to throw errors, causing the ws-appserver
     * not to be able to start, making it hard to fix the issues (e.g., database reload no longer possible).
     */
	private boolean disabled = false;
	
    public void open(Processor processor)
	{
    	activeCroms.put(processor.getConnector().getMiddleware().getName(), this);
    	this.soapProcessor = processor;
		// Re-initialize listeners
		initializeCromRegistry();
    }
    
	public void reset(Processor processor)
	{
		// Re-initialize listeners
		initializeCromRegistry();
	}
	
	public void close(Processor processor)
	{
		activeCroms.remove(processor.getConnector().getMiddleware().getName());
	}
	
	public ApplicationTransaction createTransaction(SOAPTransaction soapTransaction)
	{
		// Here we create an empty transaction. 
		//  The only thing we do extra, is that we check whether the current request
		//  comes from the CWS publish on WS-AppServer classes: in that case
		//  the ws-apps class registry is reloaded, and that means crom also has
		//  to re-initialize.
		//  So we have to inspect the call. Hopefully WS-AppServer won't change the call :)
		
		int requestEnvelope = soapTransaction.getRequestEnvelope();
		XPath xpath = XPath.getXPathInstance(".//wsapps:ReloadClassRegistry"); 
		XPathMetaInfo xpathMetaInfoObj = new XPathMetaInfo(); 
		xpathMetaInfoObj.addNamespaceBinding("wsapps", "http://schemas.cordys.com/WS-AppServer/1.0"); 
		int reloadClassesNode = xpath.firstMatch(requestEnvelope, xpathMetaInfoObj);
		if (reloadClassesNode!=0)
		{
			initializeCromRegistry();
		}
		
		// Returning "this" as ApplicationTransaction, since we don't do anything else
		//  within the transaction.
		return this;
	}
	
	private String dbPackage;
	
	/**
	 * Retrieves crom for the current WS-AppServer context.
	 * If it cannot be found, most probably crom has not been configured as an application connector
	 * on the service container.
	 * @return
	 */
	private static CROMConnector getActiveCrom()
	{
		String soapProcessorDN = BSF.getSOAPProcessor();
		CROMConnector crom = activeCroms.get(soapProcessorDN);
		if (crom==null)
		{
			throw new BsfRuntimeException("Crom has not been configured on the service container");
		}
		return crom;
	}
	
	public static String getDBPackage(CROMConnector crom)
	{
		if (crom==null)
		{
			crom = getActiveCrom();
		}
		return crom.getDBPackage();
	}
	
	public String getDBPackage()
	{
		return dbPackage + ".";
	}
	
	/**
	 * Adds an error that was discovered during initialization of crom.
	 * E.g., when crom starts it searches for @DomainObject classes, and in 
	 * each of them, there must be UID elements. If for few classes this is
	 * not available, then throwing the exception immediately on the first class
	 * means that we have to restart quite a few times to find all these errors.
	 * This method allows us to bundle that information.
	 * @param errorMessage
	 */
	void addConfigurationError(String errorMessage)
	{
		// Yes i know this is ugly. But soap processor error information is not shown nicely
		configurationErrors += ".............!! "+errorMessage+" ................!!";
	}
	
	private synchronized void initializeCromRegistry()
	{
		int configuration = super.getConfiguration();
		XPath xpath = XPath.getXPathInstance(".//Configuration"); 
		XPathMetaInfo xpathMetaInfoObj = new XPathMetaInfo(); 
		int cromNode = xpath.firstMatch(configuration, xpathMetaInfoObj);
		String domainPackage = Node.getDataElement(cromNode, "domainPackage", "");
		dbPackage = Node.getDataElement(cromNode, "dbPackage", "");
		String disabledString = Node.getDataElement(cromNode, "disabled", "false");
		this.disabled = disabledString.equalsIgnoreCase("true");

		// First clean up the old configuration. Unregister existing listeners.
		deactivateListeners();
		configurationErrors = new String();
		listeners = new HashMap<Class<? extends BusObject>, BusObjectMetadata>();
		
		if (this.disabled)
		{
			return;
		}

		// Now check the configuration.
		//  Notice that is is in principle possible that either domain or db package
		//  is the "root" package, so the string is allowed to be empty.
		//  Only if BOTH are empty, then we probably have a configuration error.
		//  Anyways both cannot be the same.
		if (domainPackage.equals(dbPackage))
		{
			if (domainPackage.trim().length()==0)
			{
				// aha: probably the values have not been set...
				throw new RuntimeException("Invalid crom configuration: domain package and db package have not been set");
			}
			throw new RuntimeException("Invalid crom configuration: domain package and db package cannot be the same");
		}

    	try
		{
    		// Possible error situations:
    		//  - no domain classes found. This is not always wrong, e.g. when only DBListeners must be registered. So assume it is only wrong if domainpackage is not empty.
    		//  - if no database classes are found, then probably there is already an error from the domain package, at least if the naming convention is used...
    		//  	so this situation still may lead to some improper stuff
    		//  - if no classes are found at all, then it does not make sense to run crom, so probably that is a mis-configuration.
    		ArrayList<Class<?>> classesInDomainPackage = getClasses(domainPackage);
    		if (classesInDomainPackage.isEmpty() && domainPackage.trim().length()>0)
    		{
    			throw new RuntimeException("Crom initialization failed because there were no classes found with domain package '"+domainPackage+"'");
    		}
    		for (Class<?> clazz : classesInDomainPackage)
    		{
    			loadDomainMetadata(clazz);
    		}
    		ArrayList<Class<?>> classesInDBPackage = getClasses(dbPackage);
    		for (Class<?> clazz : classesInDBPackage)
    		{
    			loadDBMetadata(clazz);
    		}
    		// Check if there are no domain and no database classes, probably throw an exception
    		//  that the classes cannot be found. Probably this is a mis-configuration
    		if (classesInDomainPackage.isEmpty() && classesInDBPackage.isEmpty())
    		{
    			throw new RuntimeException("Crom initialization failed because there were no classes found with domain package '"+domainPackage+"' and database package '"+dbPackage+"'");
    		}
		}
		catch (ClassNotFoundException e)
		{
			throw new RuntimeException(e);
		}
		catch (IOException e)
		{
			throw new RuntimeException(e);
		}

		if (! configurationErrors.isEmpty())
		{
			throw new Error("Crom initialization failed because of following reasons\n\n"+configurationErrors);
		}
		
		activateListeners();
	}
	
	@SuppressWarnings("unchecked") // Check is done in code
	private void loadDomainMetadata(Class<?> clazz)
	{
		if (!BusObject.class.isAssignableFrom(clazz)) // this is the check for BusObject.class
		{
			return;
		}
		
		// this is the statement on which we suppress the warnings for "unchecked"
		Class<? extends BusObject> domainClass = (Class<? extends BusObject>) clazz;
			
		// Only create a listener if the domainClass is annotated as a DomainObject,
		// and only take those classes for which class info belongs to it.
		//  E.g., generated base classes have the same ClassInfo object as their extension class,
		//  but the base classes should not be registered.
		if (BusObjectMetadata.isDomainObject(domainClass) && BusObject._getClassInfo(domainClass).getDefinedClass().equals(domainClass))
		{
			BusObjectMetadata metadata = new BusObjectMetadata(domainClass, this);
			listeners.put(domainClass, metadata);
		}
	}
	
	@SuppressWarnings("unchecked") // Check is done in code
	private void loadDBMetadata(Class<?> clazz)
	{
		if (!BusObject.class.isAssignableFrom(clazz)) // this is the check for BusObject.class
		{
			return;
		}
		
		new DBObjectEventListener((Class<? extends BusObject>) clazz);
	}

	private void activateListeners()
	{
		for (BusObjectMetadata listener : listeners.values())
		{
			listener.registerListeners();
		}
	}
	
	private void deactivateListeners()
	{
		for (BusObjectMetadata listener : listeners.values())
		{
			listener.unregisterListeners();
		}
	}
	
	/**
	 * Contains the listeners by domain class.
	 */
	private Map<Class<? extends BusObject>, BusObjectMetadata> listeners = new HashMap<Class<? extends BusObject>, BusObjectMetadata>();
	
	public static BusObjectMetadata getMetadata(Class<? extends BusObject> domainClass)
	{
		CROMConnector crom = getActiveCrom();
		crom.checkInitialization();
		return crom.listeners.get(domainClass);
	}
	
	private void checkInitialization()
	{
		if (this.disabled)
		{
			throw new BsfRuntimeException("Crom connector is disabled.");
		}
		if (listeners.isEmpty())
		{
			throw new BsfRuntimeException("Crom is not initialized. Check whether domain package and database package have been configured");
		}
	}
	
	/**
	 * Loads a class through the crom classloader (crom tries to reuse the WS-AppServer classloader because of it's hot deployment features).
	 * @param className
	 * @return
	 * @throws ClassNotFoundException
	 */
	public Class<?> loadClass(String className) throws ClassNotFoundException
	{
		return classLoader.loadClass(className);
	}
	
	/**
	 * Our own internal reference to the ws-appserver classloader...
	 */
	private ClassLoader classLoader;
	
	/**
	 * This method goes into WS-AppServer internals to try to get to the ws-appserver classloader. Ugly.
	 * If it cannot be found, then it reverts back to the default class loader.
	 */
	private void useWSAppServerClassLoader()
	{
		// This is a try out piece of code. It worked, with the exception that we cannot use
		//  the default findResources method of the classloader, which is why we had to revert
		//  to the standard class loader. Also "resetting" worked only once or so, we did not dive further into it.
		soapProcessor.getClass();
//		try
//		{
//			Field privateFieldApplicationConnectors = Processor.class.getDeclaredField("applicationConnectors");
//			privateFieldApplicationConnectors.setAccessible(true);
//			ApplicationConnector[] connectors = (ApplicationConnector[]) privateFieldApplicationConnectors.get(soapProcessor);
//			for (ApplicationConnector applicationConnector : connectors) {
//				if (applicationConnector instanceof BsfConnector)
//				{
//					BsfConnector wsAppServer = (BsfConnector) applicationConnector;
//					Method getClassLoaderMethod = BsfConnector.class.getDeclaredMethod("getCurrentClassLoader", (Class<?>[])null);
//					getClassLoaderMethod.setAccessible(true);
//	
//					classLoader = (ClassLoader) getClassLoaderMethod.invoke(wsAppServer, (Object[])null);
//					return;
//				}
//				else
//				{
//					throw new Exception();
//				}
//			}
//		}
//		catch (Exception e)
//		{
//		}
		
		// Revert to the default class loader ... we couldn't use the ws-appserver one :(
		classLoader = Thread.currentThread().getContextClassLoader();
	}
	
	/**
	 * This code is copied from {@link http://forums.sun.com/thread.jspa?threadID=341935}
	 * After that it is slightly modified to skip domain classes, and also to deal with spaces in the directory names.
	 * @param pckgname
	 * @return
	 * @throws ClassNotFoundException
	 */
	private ArrayList<Class<?>> getClasses(String pckgname) throws ClassNotFoundException, IOException
	{
		useWSAppServerClassLoader();
		ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
		// Get a File object for the package
		String path = pckgname.replace('.', '/');
		
		// TODO: getResources currently does not look into the WS-AppServer deployfolder (this method was not overridden in the WS-AppServer classloader)
		Enumeration<URL> resources = classLoader.getResources(path + "/");
		while (resources.hasMoreElements())
		{
			URL resource = resources.nextElement();
			String resourceFileName = resource.getFile();
			if (resourceFileName.indexOf("!")>=0)
			{
				// It's a jar file
				String jarName = resourceFileName.substring(0, resourceFileName.indexOf("!")).replace("%20", " ").replace("file:", "");
				Enumeration<JarEntry> entries = new java.util.jar.JarFile(jarName).entries();

				while (entries.hasMoreElements())
				{
					String fileName = entries.nextElement().getName();
					// we are only interested in .class files
					if (fileName.endsWith(".class"))
					{
						// removes the .class extension
						String className = fileName.substring(0, fileName.length() - 6).replace('/', '.');
						try
						{
							Class<?> clazz = loadClass(className); // Should probably use our own classloader here (or the one of WS-AppServer)
							Package p = clazz.getPackage();
							// if a class is in the default package, then p will be null
							if (p!=null && p.getName().equals(pckgname))
							{
								classes.add(clazz);
							}
						}
						catch (ClassNotFoundException cnfe)
						{
							// TODO: nicely treat this error?
						}
					}
				}
			}
			else
			{
				String directoryName = resource.getFile();
				File directory = new File(directoryName.replace("%20", " "));
				if (directory.exists())
				{
					// Get the list of the files contained in the package
					String[] files = directory.list();
					for (int i = 0; i < files.length; i++)
					{
						// we are only interested in .class files
						if (files[i].endsWith(".class"))
						{
							// removes the .class extension
							String className = pckgname + '.' + files[i].substring(0, files[i].length() - 6);
							try
							{
								classes.add(loadClass(className));
							}
							catch (ClassNotFoundException cnfe)
							{
								// TODO: nicely treat this error?
							}
						}
					}
				}
			}
		}
		return classes;
	}

	@Override
	public void abort() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean canProcess(String arg0)
	{
		// Irrelevant. Crom is just parasiting on WS-AppServer service container
		return false;
	}

	@Override
	public void commit()
	{
		// Irrelevant. Crom is just parasiting on WS-AppServer service container
	}

	@Override
	public boolean process(BodyBlock arg0, BodyBlock arg1)
	{
		// Irrelevant. Crom is just parasiting on WS-AppServer service container
		return false;
	}
}
